Pro ASP.NET Core MVC2(第7版)翻译

第18章:依赖注入

作者:Adam Freeman 翻译:陈广 日期:2018-9-27


本章我将描述依赖注入(dependency injection,简写 DI),这是一种帮助创建灵活应用程序和简化单元测试的技术。依赖注入无论是从为什么会有用以及它是如何执行的角度来看都可能是一个很难理解的话题。为此,我慢慢地构建,从构建应用程序组件的传统方法开始,并逐步解释依赖注入是如何工作的以及它为什么重要。表18-1是依赖注入简介。

表 18-1:依赖注入简介

问题 回答
它是什么? 依赖注入使创建松散耦合的组件变得容易,这通常意味着组件使用由接口定义的功能,而不需要对使用哪些实现类有任何第一手的了解。
它有什么用? 依赖注入可以通过更改实现定义应用程序功能接口的组件来更容易地更改应用程序的行为。它还使隔离用于单元测试的组件变得更容易。
如何使用? Startup类用于指定使用哪些实现类,以交付应用程序使用的接口指定的功能。当创建新对象(如控制器)以处理请求时,它们将自动获得所需实现类的实例。
有任何限制或缺陷? 主要的限制是类将服务声明为构造函数参数,这可能导致构造函数的唯一作用是接收依赖项并将它们分配给实例字段。
有没有其它选择? 您不必在自己的代码中使用依赖项注入,但是了解它是如何工作的很有帮助,因为 MVC 使用它向开发人员提供功能。

表18-2为本章摘要

表 18-2:本章摘要

问题 解决方案 清单
创建松散耦合组件 通过接口隔离类,并使用外部映射将它们连接到一起 9-16
在组件(例如控制器)中声明依赖项 定义组件所需类型的构造器参数 17
配置服务映射 将映射添加到Startup 18, 20–26
单元测试具有依赖关系的组件 创建服务接口的模拟实现,并在单元测试中创建组件时将其作为构造器参数传递。 19
指定创建实现对象的方式 使用适合所管理的服务的生命周期方法创建服务映射。 27-31
接收控制器中单个 action 方法的依赖项 使用 action 注入 32
手动请求控制器中的实现对象 使用HttpContext.RequestServices属性 33

准备示例项目

在本章中,我使用【ASP.NET Core Web 应用程序(.NET Core)】模板创建了一个名为 DependencyInjection 的新的空项目。清单18-1显示了 Startup 类,它配置项目的服务和中间件组件。

清单 18-1:DependencyInjection 文件夹下的 Startup.cs 文件的内容

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace DependencyInjection
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

创建模型和存储库

本章示例需要一个简单的模型,我通过创建 Models 文件夹并添加一个名为 Product.cs 的类文件,用于定义清单18-2所示的类。

清单 18-2:Models 文件夹下的 Product.cs 文件的内容

namespace DependencyInjection.Models
{
    public class Product
    {
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}

为了管理模型,我在 Models 文件夹中添加了一个名为 IRepository.cs 的类,并使用它来定义清单18-3中所示的接口。

清单 18-3:Models 文件夹下的 IRepository.cs 文件的内容

using System.Collections.Generic;

namespace DependencyInjection.Models
{
    public interface IRepository
    {
        IEnumerable<Product> Products { get; }
        Product this[string name] { get; }
        void AddProduct(Product product);
        void DeleteProduct(Product product);
    }
}

接口定义了可以在Product对象集合上执行的操作。为了提供接口的实现,我在 Models 文件夹中添加了一个名为 MemoryRepository.cs 的类文件,并定义了清单18-4所示的类。

清单 18-4:Models 文件夹下的 MemoryRepository.cs 文件的内容

using System.Collections.Generic;

namespace DependencyInjection.Models
{
    public class MemoryRepository : IRepository
    {
        private Dictionary<string, Product> products;
        public MemoryRepository()
        {
            products = new Dictionary<string, Product>();
            new List<Product> {
                new Product { Name = "Kayak", Price = 275M },
                new Product { Name = "Lifejacket", Price = 48.95M },
                new Product { Name = "Soccer ball", Price = 19.50M }
            }.ForEach(p => AddProduct(p));
        }

        public IEnumerable<Product> Products => products.Values;

        public Product this[string name] => products[name];

        public void AddProduct(Product product) =>
            products[product.Name] = product;

        public void DeleteProduct(Product product) =>
            products.Remove(product.Name);
    }
}

MemoryRepository类使用字典将其模型对象存储在内存中。这意味着没有持久存储,停止或重新启动应用程序会将模型重置为构造函数中创建的示例数据对象。对于一个真正的项目来说,这不是一种明智的方法,但对于本章来说,它就足够了,这一章的重点是应用程序工作的不同方面。

创建控制器和视图

我创建了 Controllers 文件夹,添加了一个名为 HomeController.cs 的类文件,并使用它来定义清单18-5所示的类。

清单 18-5:Controllers 文件夹下的 HomeController.cs 文件的内容

using Microsoft.AspNetCore.Mvc;

namespace DependencyInjection.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index() => View();
    }
}

控制器只有一个 action 方法,它使用View方法创建将呈现默认视图的ViewResult。为了创建与 action 方法相关的视图,我创建了 Views/Home 文件夹,并添加了一个名为 Index.cshtml 的 Razor 文件。清单18-6显示了我添加到视图中的标记。

清单 18-6:Views/Home 文件夹下的 Index.cshtml 文件的内容

@model IEnumerable<Product>
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Dependency Injection</title>
    <link rel="stylesheet" asp-href-include="lib/twitter-bootstrap/css/*.min.css" />
</head>
<body class="m-1 p-1">
    @if (ViewData.Count > 0)
    {
        <table class="table table-bordered table-sm table-striped">
            @foreach (var kvp in ViewData)
            {
                <tr><td>@kvp.Key</td><td>@kvp.Value</td></tr>
            }
        </table>
    }
    <table class="table table-bordered table-sm table-striped">
        <thead>
            <tr><th>Name</th><th>Price</th></tr>
        </thead>
        <tbody>
            @if (Model == null)
            {
                <tr><td colspan="3" class="text-center">No Model Data</td></tr>
            }
            else
            {
                @foreach (var p in Model)
                {
                    <tr>
                        <td>@p.Name</td>
                        <td>@string.Format("{0:C2}", p.Price)</td>
                    </tr>
                }
            }
        </tbody>
    </table>
</body>
</html>

视图是使用Product对象枚举的强类型视图,视图的主要内容是 HTML 表格。如果控制器不提供任何模型数据,则将消息显示为表的唯一内容。如果存在模型数据,则在枚举中为每个Product对象向表格添加一行。还有一个表,它将枚举 view bag 中的键和值(如果有的话),但在其他情况下是隐藏的。我将在本章后面使用这个表。

视图依赖于 Bootstrap CSS 包来设置 HTML 元素的样式。要将 Bootstrap 添加到项目中,我在 ControllersAndActions 项目中单击鼠标右键,在弹出菜单中选择【添加】➤【添加客户端库】,并将 twitter-bootstrap 添加至项目中。最终生成的 libman.json 配置文件代码清单18-7所示:

清单 18-7:DependencyInjection 文件夹下的 libman.json 文件的内容

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "twitter-bootstrap@4.1.3",
      "destination": "wwwroot/lib/twitter-bootstrap/"
    }
  ]
}

最后的准备工作是在 Views 文件夹中创建 _ViewImports.cshtml 文件,该文件夹设置内置的标签助手用于 Razor 视图并导入模型名称空间,如清单18-8所示。

清单 18-8:Views 文件夹下的 _ViewImports.cshtml 文件的内容

@using DependencyInjection.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

创建单元测试项目

按照第7章中描述的过程,我使用【xUnit测试项目(.NET Core)】模板创建了一个名为 DependencyInjection.Tests 的项目。我删除了 UnitTest1.cs 文件,因此项目中没有测试。

如果运行程序,将会看到如图18-1所示的结果。

图18-1 运行示例应用程序

创建松散耦合组件

图18-1没有显示模型数据的原因是,需要将模型数据传递给其视图的HomeController类与包含模型数据的MemoryRepository类之间没有关系。在 MVC 应用程序中将组件连接在一起的目的是能够轻松地使用相同功能的替代实现组件替换。

可替换组件能进行有效的单元测试,能轻松地改变应用程序在不同宿主环境(如开发和生产服务器)中的行为,并简化了长期的应用程序维护。

在接下来的部分中,我首先解释替代方法及其带来的问题。这似乎是解释依赖注入特性的一种间接方法,但 DI 的挑战之一是,它解决了一个在编写代码时并不总是很明显的问题,而且这个问题只在开发周期后期才出现。


对依赖注入的看法

依赖注入是读者最常与我联系的主题之一。大约一半的电子邮件抱怨说我将 DI “强加”给他们。奇怪的是,另一半是抱怨我没有充分强调 DI 的好处,其他读者可能还没有意识到它有多有用。

依赖注入可能是一个很难理解的话题,它的价值是有争议的。DI 可以是一个有用的工具,但并不是每个人都喜欢它-或者需要它。

如果你不做单元测试,或者如果你是在一个小的,自成一体的和稳定的项目,DI 提供的好处有限。理解 DI 的工作方式仍然很有帮助,因为 DI 用于访问一些重要的 MVC 特性,但是您并不总是需要在控制器和编写的其他类中包含 DI。\

我在自己的项目中使用 DI,很大程度上是因为我发现项目经常会朝着意想不到的方向发展,能够轻松地用新的实现替换组件可以节省大量繁琐和容易出错的更改。我宁愿在项目开始时付出一些努力,也不愿在以后做一系列复杂的编辑。我并不教条地使用依赖注入,它解决的问题并非在每个项目中会出现。只有你才能确定你的项目是否需要DI,只有你才能评估收益和成本。只有您可以确定是否需要在项目中使用 DI,只有您可以评估的利益和成本。


检查紧耦合组件

对于大多数开发人员来说,最自然的倾向是选择最直接的途径来解决问题。对于示例应用程序,这意味着使用new关键字创建控制器所需的存储库对象,以获取模型数据,如清单18-9所示。

清单 18-9:Controllers 文件夹下的 HomeController.cs 文件,实例化存储库

using Microsoft.AspNetCore.Mvc;
using DependencyInjection.Models;

namespace DependencyInjection.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index() => View(new MemoryRepository().Products);
    }
}

关于这个代码的好消息是它能工作。如果运行应用程序,您将看到浏览器中显示的模型对象的详细信息,如图18-2所示。

图18-2 显示模型数据

坏消息是 Home 控制器和MemoryRepository类现在紧密耦合,这意味着,如果不更改HomeController类,我就无法替换存储库。正如我在第7章中解释的那样,执行有效的单元测试意味着能够隔离单个组件,但如果不隐式地测试存储库类,我就无法测试清单18-9中的Index action 方法。如果我的单元测试失败,我将无法判断问题是否在控制器、存储库或存储库所依赖的其他组件中。出于现实目的,Home 控制器和MemoryRepository形成一个单独的单元,如图18-3所示。

图18-3 紧耦合组件效果

单元测试的解耦组件

在第7章中,我通过一个实现其接的属性存储对存储库类的引用,这允许我为单元测试目的创建一个模拟存储库。清单18-10显示了在本章的示例应用程序中应用于控制器的这种方法。

清单 18-10:Controllers 文件夹下的 HomeController.cs 文件,使用存储库属性

using Microsoft.AspNetCore.Mvc;
using DependencyInjection.Models;

namespace DependencyInjection.Controllers
{
    public class HomeController : Controller
    {
        public IRepository Repository { get; set; } = new MemoryRepository();

        public ViewResult Index() => View(Repository.Products);
    }
}

如果您想要进行单元测试,这种技术是完全可用的,因为它允许您在单元测试中调用 action 方法之前通过设置Repository属性来隔离控制器类。

我将一个名为 DITests.cs 的类文件添加到了 DependencyInjection.Tests 项目中,并使用它定义了清单18-11所示的单元测试,它使用Repository属性在对控制器进行操作之前设置一个伪存储库。

清单 18-11:单元测试项目下的 DITests.cs 文件,测试控制器

using DependencyInjection.Controllers;
using DependencyInjection.Models;
using Microsoft.AspNetCore.Mvc;
using Moq;
using Xunit;

namespace Tests {
    public class DITests {
        [Fact]
        public void ControllerTest() {
            // Arrange
            var data = new[] { new Product { Name = "Test", Price = 100 } };
            var mock = new Mock<IRepository>();
            mock.SetupGet(m => m.Products).Returns(data);
            HomeController controller = new HomeController {
                Repository = mock.Object
            };

            // Act
            ViewResult result = controller.Index();

            // Assert
            Assert.Equal(data, result.ViewData.Model);
        }
    }
}

Repository属性允许我隔离控制器并提供测试数据,我可以在 action 方法创建的ViewResult中检查这些数据。这只为紧耦合组件问题提供了部分解决方案,因为在应用程序运行时不能设置Repository属性。正如我在第17章中解释的那样,MVC 负责实例化控制器来处理请求,而且它对Repository属性的特殊重要性一无所知。这种技术产生的效果是,为了单元测试的目的,控制器和存储库是松散耦合的,但在应用程序运行时紧密耦合,如图18-4所示。

图18-4 添加存储库属性的效果

使用类型代理

下一个逻辑步骤是在控制器类之外决定使用哪个存储库接口的实现,并将其放在应用程序的其他地方。为了演示这是如何工作的,我向示例应用程序添加了一个 Infrastructure 文件夹,并向其添加了一个名为 TypeBroker.cs 的类文件,其内容如清单18-12所示。

清单 18-12:Infrastructure 文件夹下的 TypeBroker.cs 文件的内容

using DependencyInjection.Models;
using System;

namespace DependencyInjection.Infrastructure
{
    public static class TypeBroker
    {
        private static Type repoType = typeof(MemoryRepository);
        private static IRepository testRepo;

        public static IRepository Repository =>
            testRepo ?? Activator.CreateInstance(repoType) as IRepository;

        public static void SetRepositoryType<T>() where T : IRepository =>
            repoType = typeof(T);

        public static void SetTestObject(IRepository repo)
        {
            testRepo = repo;
        }
    }
}

TypeBroker类定义了一个Repository属性,它的返回一个实现了IRepository接口的对象。Repository属性使用的实现类由repoType字段的值确定,它默认类型为MemoryRepository,但可以通过调用SetRepositoryType方法来更改。

为了支持单元测试,SetTestObject方法允许使用指定对象。在清单18-13中,我更新了 Home 控制器,以便它从代理获得存储库对象。

清单 18-13:Controllers 文件夹下的 HomeController.cs 文件,使用类型代理

using Microsoft.AspNetCore.Mvc;
using DependencyInjection.Models;
using DependencyInjection.Infrastructure;

namespace DependencyInjection.Controllers
{
    public class HomeController : Controller
    {
        public IRepository Repository { get; } = TypeBroker.Repository;

        public ViewResult Index() => View(Repository.Products);
    }
}

现在,示例应用程序中有一组更复杂的关系集,如图18-5所示。需要注意的关键是,控制器类和存储库类之间没有直接关系 —— 所有内容都是通过接口和代理来实现的。这意味着可以更改存储库类,而不必对控制器进行任何更改。

图18-5 添加类型代理的效果

为了演示类型代理的使用,我在 Models 文件夹中添加了一个名为 AlternateRepository.cs 的类文件,并使用它来定义IRepository接口的另一个实现,如清单18-14所示。

清单 18-14:Models 文件夹下的 AlternateRepository.cs 文件的内容

using System.Collections.Generic;

namespace DependencyInjection.Models
{
    public class AlternateRepository : IRepository
    {
        private Dictionary<string, Product> products;

        public AlternateRepository()
        {
            products = new Dictionary<string, Product>();
            new List<Product> {
                new Product { Name = "Corner Flags", Price = 34.95M },
                new Product { Name = "Stadium", Price = 79500M }
            }.ForEach(p => AddProduct(p));
        }

        public IEnumerable<Product> Products => products.Values;

        public Product this[string name] => products[name];

        public void AddProduct(Product product) =>
            products[product.Name] = product;

        public void DeleteProduct(Product product) =>
            products.Remove(product.Name);
    }
}

在实际应用程序中,替代存储库可能以不同的格式存储其数据,或者使用不同类型的持久性。在本例中,AlternateRepository类和MemoryRepository类之间的区别是它们在类实例化时创建的模型数据。为了使用AlternateRepository类,我在Startup类的ConfigureServices方法中配置了类型代理,如清单18-15所示。

清单 18-15:DependencyInjection 文件夹下的 Startup.cs 文件,配置代理

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using DependencyInjection.Infrastructure;
using DependencyInjection.Models;

namespace DependencyInjection
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            TypeBroker.SetRepositoryType<AlternateRepository>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

通过启动应用程序可以看到更改的效果,该应用程序将显示新存储库类提供的数据,如图18-6所示。

图18-6 添加类型代理的效果

类型代理允许使用特定对象作为存储库,从而可以编写单元测试,如清单18-16所示。

清单 18-16:测试项目下的 DITests.cs 文件,通过代理进行测试

using DependencyInjection.Controllers;
using DependencyInjection.Infrastructure;
using DependencyInjection.Models;
using Microsoft.AspNetCore.Mvc;
using Moq;
using Xunit;

namespace Tests {
    public class DITests {
        [Fact]
        public void ControllerTest() {
            // Arrange
            var data = new[] { new Product { Name = "Test", Price = 100 } };
            var mock = new Mock<IRepository>();
            mock.SetupGet(m => m.Products).Returns(data);
            TypeBroker.SetTestObject(mock.Object);
            HomeController controller = new HomeController();

            // Act
            ViewResult result = controller.Index();

            // Assert
            Assert.Equal(data, result.ViewData.Model);
        }
    }
}

介绍 ASP.NET 依赖注入

在上一节中,我介绍了分离控制器类和提供其模型数据的存储库的过程。HomeController类现在可以在不了解使用哪个类或如何实例化的情况下获得IRepository接口的实现。TypeBroker类中包含了有关使用哪个IRepository类的知识,任何其他需要访问存储库的控制器都可以使用这个类,也可以应用于测试对象。

整体效果是一个更灵活的应用程序,但还有一些毛刺。最大的缺点是,我必须为要管理代理中的每个新类型添加新的方法和属性。我可以重写TypeBroker类以使其更通用,但是没有任何必要,因为 ASP.NET Core 提供了相同功能的更灵活的版本,打包的方式使其更易于使用,不需要任何特殊的类。

准备依赖注入

术语依赖注入(DI)描述了创建松散耦合组件的另一种方法,它集成到 ASP.NET Core 平台中,并由 MVC 自动使用,这意味着控制器和其他组件不需要了解它们所需的类型是如何创建的。清单18-17显示了如何为 DI 编写 Home 控制器。

清单 18-17:Controllers 文件夹下的 HomeController.cs 文件,准备 DI

using Microsoft.AspNetCore.Mvc;
using DependencyInjection.Models;
using DependencyInjection.Infrastructure;

namespace DependencyInjection.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;

        public HomeController(IRepository repo) => repository = repo;

        public ViewResult Index() => View(repository.Products);
    }
}

控制器将其依赖项声明为构造函数参数。这说明了术语的第一部分:依赖注入中的依赖项是创建类的新实例所需的对象。在这种情况下,控制器类已经声明了对IRepository接口的依赖关系。

在 ASP.NET Core 中,称为*服务提供者(service provider)*的组件负责将接口映射到用于满足依赖关系的实现类型。

当需要一个新的控制器时,MVC 要求服务提供者创建HomeController类的一个新实例。服务提供者检查HomeController构造函数以确定其依赖关系,创建所需的服务对象,并将它们注入HomeController构造函数,以创建可用于处理请求的新控制器。这是依赖注入的核心过程,因此为了清楚起见,我将详细说明它。

  1. MVC 接收到 Home 控制器上的 action 方法的传入请求。
  2. MVC 向 ASP.NET 服务提供者组件请求HomeController类的新实例。
  3. 服务提供者检查HomeController构造函数,发现它依赖于IRepository接口。
  4. 服务提供者咨询它的映射,以找到它被告知用于IRepository接口上的依赖项的实现类。
  5. 服务提供者创建实现类的新实例。
  6. 服务提供者使用实现对象作为构造函数参数创建一个新的HomeController对象。
  7. 服务提供者将新创建的HomeController对象返回给 MVC,MVC 使用它来处理传入 HTTP 请求。

总体效果与自定义类型代理类相同,但一个重要的优点是依赖注入过程集成到 MVC 中,这意味着每当创建控制器类时都将使用服务提供者组件。这允许控制器类声明依赖项,而不需要任何关于如何解决它们的知识。您只需编写控制器类,将它们的依赖项声明为构造函数参数,并让 MVC 和服务提供者组件解决其余的问题。

注意:本章中的所有示例都使用作为 ASP.NET Core 一部分的内置依赖注入系统。一些第三方包可以作为内置功能的插入替换,并且可以提供增强和附加功能。流行的包括 Autofac 和 StructureMap,不过在编写本报告时,还需要额外的包才能将它们集成到 ASP.NET Core 中。您可以在 http://github.com/aspnet/DependencyInjection/blob/dev/README.md 获取详细信息。

配置服务提供者

通过HomeController构造函数声明依赖关系已经破坏了应用程序,如果您运行项目,可以看到。当 MVC 试图创建HomeController类的实例以服务请求时,它会遇到如图18-7所示的错误。

图18-7 运行示例项目

要解决依赖关系,必须对服务提供程序进行配置,以便它知道如何解决服务依赖关系。目前,服务提供者没有这些信息,当被要求创建HomeController对象时,它会抛出一个异常,因为它不知道如何解决IRepository接口上的依赖关系。

服务提供者的配置是在Startup类中定义的,以便在应用程序开始接收请求之前服务已经就位。在清单18-18中,我已经配置了服务提供者,以便它知道如何处理IRepository接口上的依赖关系。

清单 18-18:DependencyInjection 文件夹下的 Startup.cs 文件,配置服务提供者

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using DependencyInjection.Infrastructure;
using DependencyInjection.Models;

namespace DependencyInjection
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IRepository, MemoryRepository>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

依赖注入是使用扩展方法来配置的,这些扩展方法是对由ConfigureServices方法接收的IServiceCollection对象调用的。我在清单中使用的AddTransient扩展方法告诉服务提供者如何处理依赖项(我将在本章后面更详细地描述)。映射使用类型参数表示,第一种类型是接口,第二种类型是实现类。

...
services.AddTransient<IRepository, MemoryRepository>();
...

此语句告诉服务提供者通过创建一个MemoryRepository对象来解决IRepository接口上的依赖关系。如果运行该应用程序,您将看到由HomeController构造函数声明的依赖被解析,并且控制器可以对模型数据进行访问了,如图18-8所示。

图18-8 配置依赖注入

具有依赖关系的控制器的单元测试

使用构造函数接收依赖项使单元测试控制器变得容易。清单18-19显示了清单18-18中控制器的单元测试。

清单 18-19:单元测试项目下的 DITests.cs 文件,配置服务提供者

using DependencyInjection.Controllers;
using DependencyInjection.Models;
using Microsoft.AspNetCore.Mvc;
using Moq;
using Xunit;

namespace Tests {
    public class DITests {
        [Fact]
        public void ControllerTest() {
            // Arrange
            var data = new[] { new Product { Name = "Test", Price = 100 } };
            var mock = new Mock<IRepository>();
            mock.SetupGet(m => m.Products).Returns(data);
            HomeController controller = new HomeController(mock.Object);

            // Act
            ViewResult result = controller.Index();

            // Assert
            Assert.Equal(data, result.ViewData.Model);
        }
    }
}

只要实现了正确的接口,控制器就不知道 —— 或者不关心 —— 向构造函数传递什么样的对象。这允许我使用我的伪存储库,而不必依赖任何可能影响测试结果的外部类,例如类型代理。

使用依赖链

当服务提供者需要解析依赖项时,它将检查已配置,用于确定是否还有要解决的依赖项的类型。其结果是您可以创建一个依赖链,所有依赖项都在运行时被解析,所有这些都可以通过Startup类中的配置来管理。为了演示依赖链,我在 Models 文件夹中添加了一个名为 IModelStorage.cs 的类文件,并使用它来定义如清单18-20所示的接口。

清单 18-20:Models 文件夹下的 IModelStorage.cs 文件的内容

using System.Collections.Generic;

namespace DependencyInjection.Models
{
    public interface IModelStorage
    {
        IEnumerable<Product> Items { get; }
        Product this[string key] { get; set; }
        bool ContainsKey(string key);
        void RemoveItem(string key);
    }
}

该接口定义了Product对象的简单存储机制的行为。为了实现这个接口,我将一个名为 DictionaryStorage.cs 的类文件添加到 Models 文件夹中,并使用它来定义清单18-21所示的类。 清单 18-21:Models 文件夹下的 DictionaryStorage.cs 文件的内容

using System.Collections.Generic;
namespace DependencyInjection.Models
{
    public class DictionaryStorage : IModelStorage
    {
        private Dictionary<string, Product> items
            = new Dictionary<string, Product>();

        public Product this[string key]
        {
            get { return items[key]; }
            set { items[key] = value; }
        }

        public IEnumerable<Product> Items => items.Values;
        public bool ContainsKey(string key) => items.ContainsKey(key);
        public void RemoveItem(string key) => items.Remove(key);
    }
}

DictionaryStorage类通过使用强类型字典来存储模型对象来实现IModelStorage接口。这是当前包含在MemoryRepository类中的功能,在实际项目中使用接口分离没有什么价值,但它提供了一个有用的示例,说明如何使用依赖注入而不给示例应用程序增加太多额外的复杂性。

在清单18-22中,我更新了MemoryRepository类,以便它声明对IModelStorage接口的依赖,而无需了解将在运行时使用的实现类。

清单 18-22:Models 文件夹下的 MemoryRepository.cs 文件,声明一个依赖项

using System.Collections.Generic;

namespace DependencyInjection.Models
{
    public class MemoryRepository : IRepository
    {
        private IModelStorage storage;

        public MemoryRepository(IModelStorage modelStore)
        {
            storage = modelStore;
            new List<Product> {
                new Product { Name = "Kayak", Price = 275M },
                new Product { Name = "Lifejacket", Price = 48.95M },
                new Product { Name = "Soccer ball", Price = 19.50M }
            }.ForEach(p => AddProduct(p));
        }

        public IEnumerable<Product> Products => storage.Items;

        public Product this[string name] => storage[name];

        public void AddProduct(Product product) =>
            storage[product.Name] = product;

        public void DeleteProduct(Product product) =>
            storage.RemoveItem(product.Name);
    }
}

如果运行应用程序,您将看到服务提供者抛出带有以下消息的异常:

InvalidOperationException: Unable to resolve service for type
'DependencyInjection.Models.IModelStorage' while attempting to activate
'DependencyInjection.Models.MemoryRepository'.

这表明服务提供者正在通过依赖链进行工作。当它被要求创建一个新的控制器时,它检查了HomeController构造函数,并找到了一个IRepository接口的依赖项,它知道这个依赖项应该用MemoryRepository对象来解析。然后,服务提供者检查了MemoryRepository构造函数,该构造函数依赖于IModelStorage接口。配置未指定IModelStorage依赖项应如何解决,这意味着无法创建MemoryRepository对象,反过来,这意味着也无法创建HomeController对象。服务提供者无法向 MVC 提供处理请求所需的对象,并引发异常。

我需要的是一个类型映射,它告诉服务提供者应该如何解决IModelStorage上的依赖关系,我已经将它添加到清单18-23中的应用程序配置中。

清单 18-23:DependencyInjection 文件夹下的 Startup.cs 文件,一个附加类型映射

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using DependencyInjection.Infrastructure;
using DependencyInjection.Models;

namespace DependencyInjection
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IRepository, MemoryRepository>();
            services.AddTransient<IModelStorage, DictionaryStorage>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

通过此添加,服务提供者可以满足链中的两个依赖项,并能够创建服务请求所需的一组对象:将DictionaryStorage对象注入到MemoryRepository构造函数中,然后将其注入 Home 控制器构造函数中。依赖链不仅仅是一个聪明的技巧;它们允许通过组合组件来组合复杂的功能,这些组件可以很容易地隔离以进行测试,并且随着项目的成熟,这些组件可以很容易地被修改以适应不断变化的需求。

对具体类型使用依赖注入

依赖注入也可以用于无法通过接口访问具体类型。虽然这没有提供使用接口的松耦合优势,但它本身是一种有用的技术,因为它允许在应用程序中的任何地方访问对象,并将具体类型置于生命周期管理之下,我在本章稍后将对此进行描述。

为了演示,我在 Models 文件夹中添加了一个名为 ProductTotalizer.cs 的类文件,并使用它来定义清单18-24所示的类。

清单 18-24:Models 文件夹下的 ProductTotalizer.cs 文件的内容

using System.Linq;

namespace DependencyInjection.Models
{
    public class ProductTotalizer
    {
        public ProductTotalizer(IRepository repo) => Repository = repo;
        public IRepository Repository { get; set; }
        public decimal Total => Repository.Products.Sum(p => p.Price);
    }
}

这个类并没有做任何特别有用的事情,但是它确实有依赖于IRepository接口,这意味着使用依赖注入来解决此依赖项与使用配置将同样适用于应用程序其余部分。在清单18-25中,我已经将ProductTotalizer类声明为HomeController类的依赖项。

清单 18-25:Controllers 文件夹下的 HomeController.cs 文件,添加依赖项

using Microsoft.AspNetCore.Mvc;
using DependencyInjection.Models;
using DependencyInjection.Infrastructure;

namespace DependencyInjection.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;
        private ProductTotalizer totalizer;

        public HomeController(IRepository repo, ProductTotalizer total)
        {
            repository = repo;
            totalizer = total;
        }

        public ViewResult Index()
        {
            ViewBag.Total = totalizer.Total;
            return View(repository.Products);
        }
    }
}

Index action 添加一个 view bag 属性,该属性包含ProductTotalizer类的产品总价,该属性将显示在本章开头添加到 Index.cshtml 视图中的 view bag 值的表中。最后一步是告诉服务提供者如何处理 ProductTotalizer 请求,如清单18-26所示。

清单 18-26:DependencyInjection 文件夹下的 Startup.cs 文件,配置服务提供者

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using DependencyInjection.Infrastructure;
using DependencyInjection.Models;

namespace DependencyInjection
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IRepository, MemoryRepository>();
            services.AddTransient<IModelStorage, DictionaryStorage>();
            services.AddTransient<ProductTotalizer>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

在这种情况下,服务类型和实现类型之间没有映射,因此存在一个AddTransient扩展方法的覆盖,该方法接受单个类型参数,该参数告诉服务提供者它应该实例化ProductTotalizer类以解析对该类型的依赖。

这种方法的优点 —— 相对于简单地在控制器中实例化具体类来说 —— 是服务提供者将解析具体类声明的任何依赖项,并且您可以更改配置,以便使用更专门的子类来解析具体类的依赖项。具体的类由服务提供者管理,并且受我在下一章中描述的生命周期特性的影响。如果运行应用程序,您将看到模型中Product对象的总价值被显示,如图18-9所示。

图18-9 对类使用依赖注入

理解服务生命周期

在上一节中,我使用了AddTransient扩展方法来告诉服务提供者应该如何处理IRepositoryIModelStorage接口的依赖项。AddTransient方法是定义类型映射的四种不同方法之一。表18-3描述了告诉服务提供者如何解决依赖关系的扩展方法。表18-3中所示的方法都使用类型参数,但也有一些扩展方法可以接受类型对象作为参数,如果需要在运行时生成映射,这些方法可能很有用。

表 18-3:服务提供者依赖项注入扩展方法

名称 描述
AddTransient<service, implType>() 此方法告诉服务提供者为服务类型上的每个依赖项创建实现类型的新实例。请参阅《使用瞬态生命周期》一节。
AddTransient() 此方法用于注册单个类型,该类型将对每个依赖项进行实例化,如《对具体类型使用依赖注入》这一节所述。
AddTransient(factoryFunc) 此方法用于注册一个工厂函数,该函数将被调用,以便为服务类型上的每个依赖项创建一个实现对象,如《使用工厂函数》一节所述。
AddScoped<service, implType>()
AddScoped()
AddScoped(factoryFunc)
这些方法告诉服务提供者重用实现类型的实例,以便与公共作用域(通常是单个 HTTP 请求)关联的组件发出的所有服务请求共享相同的对象。这些方法遵循与相应的AddTransient方法相同的模式。请参阅《使用作用域生命周期》一节。
AddSingleton<service, implType>()
AddSingleton()
AddSingleton<service(factoryFunc)
这些方法告诉服务提供者为第一个服务请求创建一个实现类型的新实例,然后对每个后续服务请求重用它。请参阅《使用单例生命周期》一节。
AddSingleton(instance) 此方法为服务提供程序提供一个对象,该对象对所有服务请求进行服务。服务提供者将不会创建任何新对象。

使用瞬态生命周期

开始使用依赖注入的最简单方法是使用AddTransient方法,它告诉服务提供者在需要解析依赖项时创建实现类型的新实例。这是Startup类中已经存在的配置,如下所示:

...
public void ConfigureServices(IServiceCollection services) {
    services.AddTransient<IRepository, MemoryRepository>();
    services.AddTransient<IModelStorage, DictionaryStorage>();
    services.AddTransient<ProductTotalizer>();
    services.AddMvc();
}
...

表18-3中描述的所有生命周期都提供了权衡。在解决依赖关系时,瞬态生命周期会导致创建实现类的新实例的成本,但优点是不必担心管理并发访问或确保对象可以安全地重用于多个请求。

为了演示瞬态生命周期,我重写了MemoryRepository类中的ToString方法,以便它生成一个全局唯一标识符(GUID),如清单18-27所示。

清单 18-27:Models 文件夹下的 MemoryRepository.cs 文件,重写 ToString

using System.Collections.Generic;

namespace DependencyInjection.Models
{
    public class MemoryRepository : IRepository
    {
        private IModelStorage storage;
        private string guid = System.Guid.NewGuid().ToString();

        public MemoryRepository(IModelStorage modelStore)
        {
            storage = modelStore;
            new List<Product> {
                new Product { Name = "Kayak", Price = 275M },
                new Product { Name = "Lifejacket", Price = 48.95M },
                new Product { Name = "Soccer ball", Price = 19.50M }
            }.ForEach(p => AddProduct(p));
        }

        public IEnumerable<Product> Products => storage.Items;

        public Product this[string name] => storage[name];

        public void AddProduct(Product product) =>
            storage[product.Name] = product;

        public void DeleteProduct(Product product) =>
            storage.RemoveItem(product.Name);

        public override string ToString()
        {
            return guid;
        }
    }
}

GUID 将便于识别MemoryRepository类的特定实例,并查看不同的生命周期方法如何改变服务提供者的行为方式。在清单18-28中,我更新了 Home 控制器上的Index action 方法,以便在 view bag 创建一个Controller属性,并将其设置为来自存储库的 GUID。

清单 18-28:Controllers 文件夹下的 HomeController.cs 文件,添加依赖项

using Microsoft.AspNetCore.Mvc;
using DependencyInjection.Models;
using DependencyInjection.Infrastructure;

namespace DependencyInjection.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;
        private ProductTotalizer totalizer;

        public HomeController(IRepository repo, ProductTotalizer total)
        {
            repository = repo;
            totalizer = total;
        }
        public ViewResult Index()
        {
            ViewBag.HomeController = repository.ToString();
            ViewBag.Totalizer = totalizer.Repository.ToString();
            return View(repository.Products);
        }
    }
}

Index action 方法将值添加到 view bag 中,它包含存储库对象的 GUID,两个存储库对象直接通过ProductTotalizer类的构造函数接收。如果运行应用程序,将看到这两个GUID是不同的,因为服务提供者已经使用AddTransient方法进行了配置,这意味着它创建了一个新的MemoryRepository对象来解析 HomeController 的依赖关系,而对于ProductTotalizer则创建了第二个MemoryRepository对象,如图18-10所示。

图18-10 瞬态生命周期的效果

每次你重载 web 页,新的 HTTP 请求都会让 MVC 创建新的HomeController,从而导致创建两个新的拥有自己的GUID 的MemoryRepository对象。

提示:GUD是唯一的 —— 或者说几乎是独一无二的,以至于没有什么真正的区别 —— 所以当您在机器上运行应用程序时,将看到不同的值。

使用工厂函数

AddTransient方法的一个版本接受一个工厂函数,该工厂函数每次依赖于服务类型时都会被调用。这允许更改所创建的对象,以便不同的依赖项接收不同类型的实例或配置不同的实例。在清单18-29中,我使用了一个工厂函数来根据运行应用程序的宿主环境选择不同的IRepository接口实现。

清单 18-29:DependencyInjection 文件夹下的 Startup.cs 文件,使用工厂

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using DependencyInjection.Infrastructure;
using DependencyInjection.Models;

namespace DependencyInjection
{
    public class Startup
    {
        private IHostingEnvironment env;

        public Startup(IHostingEnvironment hostEnv) => env = hostEnv;

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IRepository>(provider => {
                if (env.IsDevelopment())
                {
                    var x = provider.GetService<MemoryRepository>();
                    return x;
                }
                else
                {
                    return new AlternateRepository();
                }
            });
            services.AddTransient<MemoryRepository>();
            services.AddTransient<IModelStorage, DictionaryStorage>();
            services.AddTransient<ProductTotalizer>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

在第14章中,我描述了 ASP.NET Core 如何为Startup类提供服务以帮助设置应用程序,包括用于确定宿主环境的IHostingEnvironment接口的实现。您可以将这些服务作为参数接收到Configure方法,而不是ConfigureServices方法,因此我向Startup类添加了一个构造函数,它确实提供了对IHostingEnvironment对象的访问,并将其分配给一个名为env的字段。

ConfigureServices方法中,我在AddTransient方法中使用 lambda 表达式定义工厂函数。表达式接收一个 System.IServiceProvider 对象,该对象可用于使用表18-4中所示的方法创建已向服务提供者注册的其他类型的实例。

表 18-4IServiceProvider方法和扩展方法

名称 描述
GetService() 此方法使用服务提供程序创建服务类型的新实例。如果请求的类型没有映射,则返回null
GetRequiredService() 此方法使用服务提供程序创建服务类型的新实例。如果请求的类型没有映射,则引发异常。

在工厂函数中,我使用IHostingEnvironment来确定应用程序是否在开发环境中运行,如果是的话,我使用GetService方法创建MemoryRepository类的实例,并将它从工厂函数中作为对象返回给IRepository依赖项。我使用GetService创建对象,因为MemoryRepositoryIModelStorage接口上有自己的依赖关系,并且使用服务提供者创建对象意味着检测和解决依赖项将自动进行管理 —— 但这意味着我必须指定应该用于MemoryRepository的生命周期,就像这样:

...
services.AddTransient<MemoryRepository>();
...

如果没有此语句,服务提供者将无法获得创建和管理MemoryRepository对象所需的信息。

如果应用程序未在开发环境中运行,则工厂函数返回一个AlternateRepository类的新实例。这个类可以直接使用new关键字创建,因为它在构造函数中不声明任何依赖项。

使用作用域生命周期

这个生命周期从实现类中创建单个对象,用于解决与单个作用域关联的所有依赖项,这通常意味着单个 HTTP 请求(您可以创建自己的作用域,但对大多数应用程序来说这没有用)。

由于默认作用域是 HTTP 请求,因此这个生命周期允许处理请求的所有组件共享单个对象,并且在编写自定义类(如路由)时,通常用于共享公共 context 数据。作用域生命周期是通过使用AddScoped扩展方法配置服务提供者来创建的,如清单18-30所示

提示:如在表18-4中所述,AddScope方法还是使用接受工厂函数且可用于注册具体类型的的版本。这些方法的工作方式与上一节中演示的AddTransient方法相同,但明显不同之处是,它们创建的对象的生命周期是不同的。

清单 18-30:DependencyInjection 文件夹下的 Startup.cs 文件,使用作用域生命周期

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using DependencyInjection.Infrastructure;
using DependencyInjection.Models;

namespace DependencyInjection
{
    public class Startup
    {
        private IHostingEnvironment env;

        public Startup(IHostingEnvironment hostEnv) => env = hostEnv;

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IRepository, MemoryRepository>();
            services.AddTransient<IModelStorage, DictionaryStorage>();
            services.AddTransient<ProductTotalizer>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

在示例应用程序中,HomeControllerProductTotalizer一起实例化以处理请求,两者都需要服务存储库来解析对IRepository接口的依赖。使用AddScoped方法可以确保两个对象的依赖性都通过一个MemoryRepository对象来解决。

通过运行该示例可以看到效果;浏览器显示的两个 GUID 都是相同的,如图18-11所示。重新加载页面将创建一个新的 HTTP 请求,这意味着将创建一个新的MemoryRepository对象。

图18-11 作用域生命周期的效果

使用单例生命周期

单例生命周期确保单个对象用于解决给定服务类型的所有依赖项。在使用此生命周期时,必须确保用于解决依赖关系的实现类对于并发访问是安全的。在清单18-31中,我已经更改了IRepository配置的作用域。

清单 18-31:DependencyInjection 文件夹下的 Startup.cs 文件,使用单例生命周期

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using DependencyInjection.Infrastructure;
using DependencyInjection.Models;

namespace DependencyInjection
{
    public class Startup
    {
        private IHostingEnvironment env;

        public Startup(IHostingEnvironment hostEnv) => env = hostEnv;

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IRepository, MemoryRepository>();
            services.AddTransient<IModelStorage, DictionaryStorage>();
            services.AddTransient<ProductTotalizer>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

AddSingleton方法在第一次解析IRepository接口上的依赖时创建MemoryRepository类的新实例,然后重用该实例用于任何后续依赖项,即使它们与不同的 HTTP 请求相关联,如图18-12所示。

图18-12 单例生命周期的效果

使用 Action 注入

声明依赖项的标准方法是通过构造函数,这是一种可以在任何类中使用的技术,它依赖于作为 ASP.NET 平台的核心部分的依赖项注入功能。

MVC 以一种称为 Action 注入的替代方法来补充标准功能,该方法允许通过 action 方法的参数来声明依赖项。严格地说,action 注入是由我在第26章中描述的模型绑定系统提供的,但是我在本章中对它进行了描述,因为它允许以不同的方式使用服务。action 注入使用FromServices特性执行,该特性应用于 action 方法参数,如清单18-32所示。

清单 18-32:Controllers 文件夹下的 HomeController.cs 文件,使用 Action 注入

using Microsoft.AspNetCore.Mvc;
using DependencyInjection.Models;
using DependencyInjection.Infrastructure;

namespace DependencyInjection.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;
        private ProductTotalizer totalizer;

        public HomeController(IRepository repo, ProductTotalizer total)
        {
            repository = repo;
            totalizer = total;
        }

        public ViewResult Index([FromServices]ProductTotalizer totalizer)
        {
            ViewBag.HomeController = repository.ToString();
            ViewBag.Totalizer = totalizer.Repository.ToString();
            return View(repository.Products);
        }
    }
}

MVC 使用服务提供者获取ProductTotalizer类的实例,并在调用Index action 方法时将其作为参数提供。与标准构造函数注入相比,使用 action 注入不太常见,但如果您依赖于一个对象,而该对象的创建成本很高,并且只需要在控制器定义的一个 action 方法中使用该对象,则使用 action 注入是非常有用的。使用构造函数注入解决了所有 action 方法的依赖,即使用于处理请求的方法不使用实现对象。用FromServices特性修饰 action 方法会缩小依赖关系的焦点,并确保只有在需要时才实例化实现类型。

使用属性注入特性

在第17章中,我解释了如何在 POCO 控制器中通过声明一个属性并用ControllerContext特性装饰它以接收 context 数据。现在您已经阅读了本章,您将了解这是一种特殊的依赖注入形式。它被称为属性注入

MVC 提供了一组专门的特性,可以通过控制器和视图组件中的属性注入来接收特定类型(我在第22章中对此进行了描述)。如果从Controller基类派生控制器,则不需要使用这些特性,因为 context 信息是通过便捷属性公开的,但是表18-5列出了 POCO 控制器中使用的特性。

表 18-5:专用属性注入特性

名称 描述
ControllerContext 该特性设置一个ControllerContext属性,该属性提供ActionContext类的功能的超集,如第31章所述。
ActionContext 此特性设置ActionContext属性以向 action 方法提供 context 信息。Controller类通过ActionContext属性以及第31章描述的一组便捷属性公开 context 信息。
ViewContext 此特性设置ViewContext属性以提供视图操作的 context 数据,包括标签助手(如第23章所述)。
ViewComponentContext 此特性为视图组件设置了ViewComponentContext属性,我在第22章中对此进行了描述。
ViewDataDictionary 如第26章所述,此特性设置ViewDataDictionary属性以提供对模型绑定数据的访问。

手动请求实现对象

ASP.NET 的主要依赖项注入特性和 MVC 为属性和 action 注入提供的附加特性提供了大多数应用程序创建松散耦合组件所需的所有支持。但是,在某些情况下,可以在不依赖于注入的情况下为接口创建一个实现。在这些情况下,您可以直接使用服务提供者,如清单18-33所示。

清单 18-33:Controllers 文件夹下的 HomeController.cs 文件,直接使用服务提供者

using Microsoft.AspNetCore.Mvc;
using DependencyInjection.Models;
using DependencyInjection.Infrastructure;
using Microsoft.Extensions.DependencyInjection;

namespace DependencyInjection.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index([FromServices]ProductTotalizer totalizer)
        {
            IRepository repository =
                HttpContext.RequestServices.GetService<IRepository>();

            ViewBag.HomeController = repository.ToString();
            ViewBag.Totalizer = totalizer.Repository.ToString();
            return View(repository.Products);
        }
    }
}

由相同名称的属性返回的HttpContext对象定义了返回IServiceProvider对象的RequestServices方法,在该方法中,可以调用表18-4中描述的方法。在清单中,我删除了使用属性注入设置的Repository属性,并使用HttpContext.RequestServices属性来获得IRepository接口的实现。

这被称为服务定位器模式(service locator pattern),一些开发人员认为应该避免这种模式。Mark Seemann 在http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern上很好地描述了它可能引发的问题。我的观点更为宽松,因为当通过构造函数接收依赖项的正常技术由于某种原因无法使用时,以这种方式获得服务是完全合理的。

总结

在本章中,我解释了依赖注入在 MVC 应用程序中扮演的角色,它帮助创建松散耦合的组件,这些组件可以很容易地替换和隔离以进行测试。我演示了这个 ASP.NET Core 依赖注入特性和 MVC 为将依赖注入到属性和 action 方法中提供的属性。我描述了在配置服务提供者时可用的不同生命周期选项,并解释了它们如何影响创建对象的方式。在下一章中,我介绍了将额外逻辑添加到请求处理进程中的过滤器。

;

© 2018 - IOT小分队文章发布系统 v0.3